//
//  LcRequestCenter.m
//  18Birdies
//
//  Created by Lc on 15/3/23.
//  Copyright (c) 2015年 Lc. All rights reserved.
//

#import "LcApiError.h"
#import "LcJson.h"
#import "LcJsonException.h"
#import "LcLog.h"
#import "LcMultipartRequestData.h"
#import "LcNetworkCenter.h"
#import "LcPersistenceCenter.h"
#import "LcPreconditions.h"
#import "LcReflection.h"
#import "LcRequestCenter.h"
#import "LcRequestData.h"
#import "LcRequestException.h"
#import "LcRequestMeta.h"
#import "LcResponseData.h"
#import "LcSecurityCenter.h"
#import "LcSecurityException.h"
#import "NSString+Lc.h"

@interface LcRequestCenter ()
@property(nonatomic, copy) NSString* baseUrl;
@property(nonatomic, strong) NSMutableDictionary* requestMetas;
@property(nonatomic, copy) NSDictionary* commonHeaders;
@property(nonatomic, copy) NSString* rsaKey;
@property(nonatomic, copy) NSString* aesIv;
@property(nonatomic, copy) NSData* commonAesKey;
@property(nonatomic, copy) NSData* userAesKey;
@property(nonatomic, copy) NSString* clientToken;
@property(nonatomic, strong) LcJson* lcJson;
@property(nonatomic, strong) LcSecurityCenter* securityCenter;
@property(nonatomic, strong) LcNetworkCenter* networkCenter;
@property(nonatomic, strong) LcPersistenceCenter* persistenceCenter;

@property(nonatomic, strong) ApiErrorHandler commonApiErrorHandler;
@property(nonatomic, strong) NetworkErrorHandler commonNetworkErrorHandler;
@end

@implementation LcRequestCenter

#pragma mark - Single Instance

- (void)internalInit {
  self.requestMetas = [NSMutableDictionary dictionary];
  self.lcJson = [LcJson sharedInstance];
  self.securityCenter = [LcSecurityCenter sharedInstance];
  self.networkCenter = [LcNetworkCenter sharedInstance];
  self.persistenceCenter = [LcPersistenceCenter sharedInstance];
}

#pragma mark -

- (void)registerBaseUrl:(NSString*)baseUrl {
  self.baseUrl = baseUrl;
}

- (void)registerRequestMeta:(LcRequestMeta*)requestMeta {
  LC_CHECK_NOT_NULL(requestMeta);
  if (![LcReflection isType:requestMeta.requestDataType
                KindOfClass:LcRequestData.class]) {
    @throw [LcRequestException
        exceptionWithReason:
            [NSString stringWithFormat:@"request data type not valid, type=%@",
                                       requestMeta.requestDataType]];
  }
  if (![LcReflection isType:requestMeta.responseDataType
                KindOfClass:LcResponseData.class]) {
    @throw [LcRequestException
        exceptionWithReason:
            [NSString stringWithFormat:@"response data type not valid, type=%@",
                                       requestMeta.responseDataType]];
  }
  [self.requestMetas setObject:requestMeta forKey:requestMeta.requestDataType];
}

- (void)registerCommonHeaders:(NSDictionary*)commonHeaders {
  self.commonHeaders = commonHeaders;
}

- (void)registerAesIv:(NSString*)aesIv {
  LC_CHECK_NOT_EMPTY(aesIv);
  self.aesIv = aesIv;
}

- (void)registerCommonAesKey:(NSData*)commonAesKey {
  LC_CHECK_NOT_EMPTY(commonAesKey);
  self.commonAesKey = commonAesKey;
}

- (void)registerRsaKey:(NSString*)rsaKey {
  LC_CHECK_NOT_EMPTY(rsaKey);
  self.rsaKey = rsaKey;
}

- (void)registerUserAesKey:(NSData*)userAesKey
               clientToken:(NSString*)clientToken {
  if (userAesKey.length > 0) {
    LC_CHECK_NOT_EMPTY(clientToken);
  }
  self.userAesKey = userAesKey;
  self.clientToken = clientToken;
}

- (void)sendRequest:(LcRequestData*)requestData
     successHandler:(SuccessHandler)successHandler
    apiErrorHandler:(ApiErrorHandler)apiErrorHandler
networkErrorHandler:(NetworkErrorHandler)networkErrorHandler {
  LC_CHECK_NOT_NULL(requestData);
  LC_CHECK_NOT_NULL(successHandler);
  LcRequestMeta* requestMeta =
      self.requestMetas[NSStringFromClass(requestData.class)];
  if (requestMeta == nil) {
    @throw [LcRequestException
        exceptionWithReason:
            [NSString stringWithFormat:
                          @"unknown request type %@, please call "
                          @"registerRequestMeta: to register it before use",
                          requestMeta.requestDataType]];
  }

  BOOL useUserAesKey = NO;
  NSData* aesKey = nil;
  switch (requestMeta.encryptionType) {
    case LcRequestEncryptionTypeRsa:
      LC_CHECK_NOT_EMPTY(self.rsaKey);
      aesKey = self.securityCenter.randomAes128Key;
      break;
    case LcRequestEncryptionTypeAesCommon:
      LC_CHECK_NOT_EMPTY(self.commonAesKey);
      aesKey = self.commonAesKey;
      break;
    case LcRequestEncryptionTypeAesUser:
      aesKey = self.userAesKey;
      useUserAesKey = YES;
      break;
    case LcRequestEncryptionTypeAesUserOrCommon:
      if (self.userAesKey.length > 0) {
        aesKey = self.userAesKey;
        useUserAesKey = YES;
      } else {
        LC_CHECK_NOT_EMPTY(self.commonAesKey);
        aesKey = self.commonAesKey;
      }
      break;
    default:
      break;
  }

  NSString* clientToken = self.clientToken;
  if (useUserAesKey) {
    if (aesKey.length == 0 || clientToken.length == 0) {
      ApiErrorHandler errorHandler = apiErrorHandler;
      if (errorHandler == nil) {
        errorHandler = self.commonApiErrorHandler;
      }
      if (errorHandler != nil) {
        LcApiError* apiError =
            [LcApiError apiErrorWithReasonCode:LcApiError_NOT_AUTHORIZED
                                        reason:nil];
        [[NSOperationQueue mainQueue] addOperationWithBlock:^{
          errorHandler(requestData, apiError);
        }];
        return;
      }
    }
  }

  NSData* fileData = nil;
  if ([requestData isKindOfClass:LcMultipartRequestData.class]) {
    LcMultipartRequestData* multipartRequestData =
        (LcMultipartRequestData*)requestData;
    fileData = multipartRequestData.fileData;
    multipartRequestData.fileData = nil;
  }
  BOOL requestCompressed = NO;
  NSData* json = [self.lcJson toJson:requestData];
  LcLog(@"requestData=%@, %@", NSStringFromClass(requestData.class),
        [NSString stringWithData:json]);
  LC_CHECK_NOT_EMPTY(json);
  if ([requestData isKindOfClass:LcMultipartRequestData.class]) {
    LcMultipartRequestData* multipartRequestData =
        (LcMultipartRequestData*)requestData;
    multipartRequestData.fileData = fileData;
  }
  if (json.length > 300) {
    json = [self.persistenceCenter compressGzip:json];
    requestCompressed = YES;
  }
  NSData* encrypted =
      aesKey == nil ? json : [self.securityCenter encrypt:json
                                            withAes128Key:aesKey
                                                       iv:self.aesIv];

  NSString* url = requestMeta.path;
  if (self.baseUrl != nil) {
    url = [self.baseUrl stringByAppendingString:requestMeta.path];
  }

  NSMutableDictionary* headers = [self.commonHeaders mutableCopy];
  if (headers == nil) {
    headers = [NSMutableDictionary dictionary];
  }
  headers[@"Compressed"] = requestCompressed ? @"true" : @"false";
  if (requestMeta.encryptionType == LcRequestEncryptionTypeRsa) {
    NSData* aesKeyData =
        [self.securityCenter encrypt:aesKey withRsaPubicKey:self.rsaKey];
    headers[@"EncryptionKey"] = [self.securityCenter base64Encode:aesKeyData];
  } else if (useUserAesKey) {
    headers[@"Token"] = clientToken;
  }

  NSData* body = nil;
  if ([LcReflection isType:requestMeta.requestDataType
               KindOfClass:LcMultipartRequestData.class]) {
    headers[@"Content-Type"] =
        [NSString stringWithFormat:@"multipart/form-data; boundary=%@",
                                   self.networkCenter.multipartBoundary];
    NSDictionary* stringParts = @{
      @"apiRequestData" : [self.securityCenter base64Encode:encrypted]
    };
    LcMultipartRequestData* multipartRequestData =
        (LcMultipartRequestData*)requestData;
    if (multipartRequestData.relativeFileName != nil) {
      body = [self.networkCenter
          createMultipartBodyWithStrings:stringParts
                                andFiles:@{
                                  @"file" : [self.persistenceCenter
                                                 .rootDirectory
                                      stringByAppendingPathComponent:
                                          multipartRequestData.relativeFileName]
                                }];
    } else if (multipartRequestData.fileData != nil) {
      body = [self.networkCenter
          createMultipartBodyWithStrings:stringParts
                                andDatas:@{
                                  @"file" : multipartRequestData.fileData
                                }];
    } else {
      body = [self.networkCenter createMultipartBodyWithStrings:stringParts
                                                       andFiles:nil];
    }
  } else {
    headers[@"Content-Type"] = @"application/binary";
    body = encrypted;
  }

  [self.networkCenter
              httpPost:url
              withBody:body
               headers:headers
      completionHandler:^(NSHTTPURLResponse* response, NSData* responseBody,
                         NSError* error) {
        if (error == nil) {
          BOOL encryptionError = NO;
          BOOL jsonError = NO;
          BOOL alreadyChanged = NO;
          if (useUserAesKey && aesKey != self.userAesKey) {
            // user already logged out or changed
            alreadyChanged = YES;
          }
          if (!alreadyChanged && response.statusCode == 200) {
            // success
            @try {
              NSData* json = aesKey == nil
                                 ? responseBody
                                 : [self.securityCenter decrypt:responseBody
                                                  withAes128Key:aesKey
                                                             iv:self.aesIv];
              NSString* compressed = response.allHeaderFields[@"Compressed"];
              if ([compressed.lowercaseString isEqualToString:@"true"]) {
                json = [self.persistenceCenter uncompressGzip:json];
              }
              LcLog(@"response json=%@", [NSString stringWithData:json]);
              LcResponseData* responseData = [self.lcJson
                  fromJson:json
                   ofClass:NSClassFromString(requestMeta.responseDataType)];
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wundeclared-selector"
              if ([responseData respondsToSelector:@selector(setResponseJsonData:)]) {
                [responseData setValue:json forKey:@"responseJsonData"];
              }
#pragma clang diagnostic pop
              [[NSOperationQueue mainQueue] addOperationWithBlock:^{
                successHandler(requestData, responseData, aesKey);
              }];
              return;
            } @catch (LcSecurityException* exception) {
#ifdef DEBUG
              @throw exception;
#else
              encryptionError = YES;
#endif
            } @catch (LcJsonException* exception) {
#ifdef DEBUG
              @throw exception;
#else
              jsonError = YES;
#endif
            }
          }

          // api error
          ApiErrorHandler errorHandler = apiErrorHandler;
          if (errorHandler == nil) {
            errorHandler = self.commonApiErrorHandler;
          }
          if (errorHandler != nil) {
            LcApiError* apiError = nil;
            if (encryptionError) {
              apiError =
                  [LcApiError apiErrorWithReasonCode:LcApiErrorEncryptionError
                                              reason:nil];
            } else if (jsonError) {
              apiError = [LcApiError apiErrorWithReasonCode:LcApiErrorJsonError
                                                     reason:nil];
            } else if (alreadyChanged) {
              apiError = [LcApiError
                  apiErrorWithReasonCode:LcApiErrorUserAlreadyChanged
                                  reason:nil];
            } else {
              @try {
                LC_CHECK_NOT_EMPTY(self.commonAesKey);
                NSData* json = [self.securityCenter decrypt:responseBody
                                              withAes128Key:self.commonAesKey
                                                         iv:self.aesIv];
                LcLog(@"api error json=%@", [NSString stringWithData:json]);
                if ([NSString stringWithData:json] == nil){
                  apiError =
                  [LcApiError apiErrorWithReasonCode:LcApiError_INTERNAL
                                              reason:nil];
                }else {
                  apiError = [self.lcJson fromJson:json ofClass:LcApiError.class];
                }
              } @catch (LcFoundationException* exception) {
                apiError =
                    [LcApiError apiErrorWithReasonCode:LcApiError_INTERNAL
                                                reason:nil];
              }
            }
            [[NSOperationQueue mainQueue] addOperationWithBlock:^{
              errorHandler(requestData, apiError);
            }];
          }
        } else {
          // network error
          NetworkErrorHandler errorHandler = networkErrorHandler;
          if (errorHandler == nil) {
            errorHandler = self.commonNetworkErrorHandler;
          }
          if (errorHandler != nil) {
            [[NSOperationQueue mainQueue] addOperationWithBlock:^{
              errorHandler(requestData, error);
            }];
          }
        }
      }];
}

- (void)registerCommonApiErrorHandler:(ApiErrorHandler)apiErrorHandler {
  self.commonApiErrorHandler = apiErrorHandler;
}

- (void)registerCommonNetworkErrorHandler:
    (NetworkErrorHandler)networkErrorHandler {
  self.commonNetworkErrorHandler = networkErrorHandler;
}

@end
